/*
 * Copyright 2008 Zoundry LLC, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 */
package org.zoundry.gaedt.ui.editors;

import java.util.Map;
import org.apache.commons.lang.ArrayUtils;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ITreeSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TreeSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.custom.StackLayout;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.dialogs.FilteredTree;
import org.eclipse.ui.dialogs.PatternFilter;
import org.eclipse.ui.forms.IManagedForm;
import org.eclipse.ui.forms.editor.FormEditor;
import org.eclipse.ui.forms.widgets.FormToolkit;
import org.eclipse.ui.forms.widgets.ScrolledForm;
import org.eclipse.ui.forms.widgets.Section;
import org.zoundry.gaedt.core.models.application.IZGaeHandlerModel;
import org.zoundry.gaedt.core.models.application.TZGaeHandlerLoginType;
import org.zoundry.gaedt.core.models.application.TZGaeHandlerType;
import org.zoundry.gaedt.core.models.application.ZGaeAbstractHandlerModel;
import org.zoundry.gaedt.core.models.application.ZGaeScriptHandlerModel;
import org.zoundry.gaedt.core.models.application.ZGaeStaticDirectoryHandlerModel;
import org.zoundry.gaedt.core.models.application.ZGaeStaticFilePatternHandlerModel;
import org.zoundry.gaedt.core.util.ZGaeUtil;
import org.zoundry.gaedt.ui.ZGaeColorUtil;
import org.zoundry.gaedt.ui.editors.application.ZGaeHandlerComparator;
import org.zoundry.gaedt.ui.editors.application.ZGaeHandlerContentProvider;
import org.zoundry.gaedt.ui.editors.application.ZGaeHandlerLabelProvider;
import org.zoundry.gaedt.ui.editors.application.ZGaeHandlerPatternFilter;
import org.zoundry.gaedt.ui.editors.dialogs.ZGaeAddHandlerDialog;
import org.zoundry.gaedt.ui.editors.dialogs.ZGaeAddHandlerInfo;
import org.zoundry.gaedt.ui.editors.operations.IZGaeUIUpdater;
import org.zoundry.gaedt.ui.editors.operations.IZGaeUndoableOperation;
import org.zoundry.gaedt.ui.editors.operations.ZGaeAddHandlerOperation;
import org.zoundry.gaedt.ui.editors.operations.ZGaeMoveHandlerOperation;
import org.zoundry.gaedt.ui.editors.operations.ZGaeRemoveHandlerOperation;
import org.zoundry.gaedt.ui.editors.operations.ZGaeSetBeanPropertyOperation;
import org.zoundry.gaedt.ui.editors.operations.ZGaeUIUpdaterAdapter;


/**
 * The "Handlers" page in the GAE Application form editor.
 */
public class ZGaeApplicationHandlersPage extends ZGaeAbstractApplicationEditorPage
{
    private static Map<TZGaeHandlerLoginType, Integer> sLoginMap = ArrayUtils.toMap(
            new Object[][] {
                    {TZGaeHandlerLoginType.None, 0},
                    {TZGaeHandlerLoginType.Required, 1},
                    {TZGaeHandlerLoginType.Admin, 2} });
    private static Map<Integer, TZGaeHandlerLoginType> sLoginMapInv = ArrayUtils.toMap(
            new Object[][] {
                    {0, TZGaeHandlerLoginType.None},
                    {1, TZGaeHandlerLoginType.Required},
                    {2, TZGaeHandlerLoginType.Admin} });

    /** The list of handlers. */
    private FilteredTree mHandlerList;
    /** The button used to add a new handler. */
    private Button mAddButton;
    /** The button to remove the selected handler. */
    private Button mRemoveButton;
    /** The button to move a handler up in the list. */
    private Button mMoveUpButton;
    /** The button to move a handler down in the list. */
    private Button mMoveDownButton;
    /** The parent composite for the various handler detail composites. */
    private Composite mHandlerDetailComposite;
    /** The composite containing the controls shown when no handler is selected. */
    private Composite mNoHandlerDetailComposite;
    /** The composite containing the script handler controls. */
    private Composite mScriptHandlerDetailComposite;
    /** The composite containing the static directory handler controls. */
    private Composite mStaticDirHandlerDetailComposite;
    /** The composite containing the static file pattern handler controls. */
    private Composite mStaticFilesHandlerDetailComposite;

    /** The most recently selected script handler model. */
    private IZGaeHandlerModel mScriptHandlerModel;
    /** The most recently selected static directory handler model. */
    private IZGaeHandlerModel mStaticDirectoryHandlerModel;
    /** The most recently selected static file pattern handler model. */
    private IZGaeHandlerModel mStaticFilesHandlerModel;

    /** The Text control for editing the script in a script handler. */
    private Text mScriptText;
    /** The Combo control for editing the login setting of a script handler. */
    private Combo mScriptLoginCombo;

    /** The Text control for editing the directory of a static directory handler. */
    private Text mStaticDirDirectoryText;
    /** The Combo control for editing the login setting of a static directory handler. */
    private Combo mStaticDirLoginCombo;
    /** The Text control for editing the mime type of a static directory handler. */
    private Text mStaticDirMimeTypeText;
    /** The Text control for editing the expiration of a static directory handler. */
    private Text mStaticDirExpirationText;

    /** The Text control for editing the pattern of a static file pattern handler. */
    private Text mStaticFilesPatternText;
    /** The Combo control for editing the login setting of a static file pattern handler. */
    private Combo mStaticFilesLoginCombo;
    /** The Text control for editing the upload regexp setting of a static file pattern handler. */
    private Text mStaticFilesUploadText;
    /** The Text control for editing the mime type of a static file pattern handler. */
    private Text mStaticFilesMimeTypeText;
    /** The Text control for editing the expiration of a static file pattern handler. */
    private Text mStaticFilesExpirationText;

    /**
     * C'tor.
     *
     * @param aEditor
     */
    public ZGaeApplicationHandlersPage(FormEditor aEditor)
    {
        super(aEditor, ZGaeApplicationHandlersPage.class.getName(), "Handlers");
    }

    /**
     * @see org.eclipse.ui.forms.editor.FormPage#createFormContent(org.eclipse.ui.forms.IManagedForm)
     */
    @Override
    protected void createFormContent(IManagedForm aManagedForm)
    {
        ScrolledForm form = aManagedForm.getForm();
        FormToolkit toolkit = aManagedForm.getToolkit();
        form.setText(getTitle());
        toolkit.decorateFormHeading(form.getForm());
        form.getBody().setLayout(new FillLayout());

        // Create a layer for handling scrolling
        ScrolledComposite scrolledComposite = new ScrolledComposite(form.getBody(), SWT.H_SCROLL
                | SWT.V_SCROLL);

        Composite pageComposite = toolkit.createComposite(scrolledComposite, SWT.NONE);
        GridLayout layout = new GridLayout();
        layout.numColumns = 2;
        layout.makeColumnsEqualWidth = true;
        pageComposite.setLayout(layout);
        GridData data = new GridData(GridData.VERTICAL_ALIGN_FILL | GridData.HORIZONTAL_ALIGN_FILL);
        pageComposite.setLayoutData(data);
        // composite.setToolTipText(PAGE_DESCRIPTION);

        scrolledComposite.setContent(pageComposite);
        scrolledComposite.setMinSize(640, 480);
        scrolledComposite.setExpandHorizontal(true);
        scrolledComposite.setExpandVertical(true);

        // Create the handlers list section
        ////////////////////////////////////
        Section handlersSection = toolkit.createSection(pageComposite, Section.TITLE_BAR);
        handlersSection.setText("Handlers");
        data = new GridData(GridData.FILL_BOTH);
        handlersSection.setLayoutData(data);
        handlersSection.setLayout(new GridLayout());
        Label label = toolkit.createLabel(handlersSection, "List of all handlers found in the app.yaml file.");
        handlersSection.setDescriptionControl(label);

        Composite handlersComposite = toolkit.createComposite(handlersSection);
        layout = new GridLayout(2, false);
        handlersComposite.setLayout(layout);
        handlersSection.setClient(handlersComposite);

        // Create the handlers list
        mHandlerList = createHandlersControl(handlersComposite);
        data = new GridData(GridData.FILL_BOTH);
        mHandlerList.setLayoutData(data);
        Composite handlerButtons = createHandlerButtons(handlersComposite, toolkit);
        data = new GridData(GridData.FILL_VERTICAL);
        handlerButtons.setLayoutData(data);

        // Create the handler detail section
        ///////////////////////////////////////
        Section handlerDetailSection = toolkit.createSection(pageComposite, Section.TITLE_BAR);
        handlerDetailSection.setText("Handler Details");
        data = new GridData(GridData.FILL_HORIZONTAL | GridData.VERTICAL_ALIGN_BEGINNING);
        handlerDetailSection.setLayoutData(data);
        handlerDetailSection.setLayout(new GridLayout());
        label = toolkit.createLabel(handlerDetailSection, "Details about the handler selected on the left.");
        handlerDetailSection.setDescriptionControl(label);

        mHandlerDetailComposite = toolkit.createComposite(handlerDetailSection);
        mHandlerDetailComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
        StackLayout slayout = new StackLayout();
        mHandlerDetailComposite.setLayout(slayout);
        handlerDetailSection.setClient(mHandlerDetailComposite);

        mNoHandlerDetailComposite = createNoHandlerSelectedUI(mHandlerDetailComposite, toolkit);
        mScriptHandlerDetailComposite = createScriptHandlerUI(mHandlerDetailComposite, toolkit);
        mStaticDirHandlerDetailComposite = createStaticDirectoryHandlerUI(mHandlerDetailComposite, toolkit);
        mStaticFilesHandlerDetailComposite = createStaticFilePatternHandlerUI(mHandlerDetailComposite, toolkit);

        showHandlerUI(mNoHandlerDetailComposite);

        // Set up listeners
        configureChangeListeners();

        updateButtonStates();
    }

    /**
     * Updates the handler UI - showing the correct composite based on the given
     * tree selection.
     *
     * @param aSelection
     */
    protected void updateHandlerUI(ITreeSelection aSelection)
    {
        if (aSelection.isEmpty())
        {
            showHandlerUI(mNoHandlerDetailComposite);
        }
        else
        {
            IZGaeHandlerModel handlerModel = (IZGaeHandlerModel) aSelection.getFirstElement();
            if (handlerModel instanceof ZGaeScriptHandlerModel)
            {
                populateScriptHandlerUI((ZGaeScriptHandlerModel) handlerModel);
                showHandlerUI(mScriptHandlerDetailComposite);
                mScriptHandlerModel = handlerModel;
            }
            else if (handlerModel instanceof ZGaeStaticDirectoryHandlerModel)
            {
                populateStaticDirectoryHandlerUI((ZGaeStaticDirectoryHandlerModel) handlerModel);
                showHandlerUI(mStaticDirHandlerDetailComposite);
                mStaticDirectoryHandlerModel = handlerModel;
            }
            else if (handlerModel instanceof ZGaeStaticFilePatternHandlerModel)
            {
                populateStaticFilePatternHandlerUI((ZGaeStaticFilePatternHandlerModel) handlerModel);
                showHandlerUI(mStaticFilesHandlerDetailComposite);
                mStaticFilesHandlerModel = handlerModel;
            }
        }
    }

    /**
     * Called to change which handler UI composite is visible.
     */
    protected void showHandlerUI(Composite aUIDetailComposite)
    {
        ((StackLayout) mHandlerDetailComposite.getLayout()).topControl = aUIDetailComposite;
        mHandlerDetailComposite.layout();
    }

    /**
     * Updates the states of the various buttons - Add, Remove, Up, Down
     * based on the current selection.
     */
    protected void updateButtonStates()
    {
        // If there is some filtering going on, disable all the buttons
        if (!"".equals(mHandlerList.getFilterControl().getText()))
        {
            mAddButton.setEnabled(false);
            mRemoveButton.setEnabled(false);
            mMoveUpButton.setEnabled(false);
            mMoveDownButton.setEnabled(false);
            return;
        }

        mAddButton.setEnabled(true);
        mRemoveButton.setEnabled(true);
        mMoveUpButton.setEnabled(true);
        mMoveDownButton.setEnabled(true);

        TreeSelection selection = (TreeSelection) mHandlerList.getViewer().getSelection();
        if (selection.size() == 0)
        {
            mRemoveButton.setEnabled(false);
            mMoveUpButton.setEnabled(false);
            mMoveDownButton.setEnabled(false);
        }
        IZGaeHandlerModel model = (IZGaeHandlerModel) selection.getFirstElement();
        if (getModel().isFirstHandler(model))
            mMoveUpButton.setEnabled(false);
        else if (getModel().isLastHandler(model))
            mMoveDownButton.setEnabled(false);
    }

    /**
     * Creates the UI that will be shown when no handler is selected.
     *
     * @param aHandlerDetailComposite
     * @param aToolkit
     */
    protected Composite createNoHandlerSelectedUI(Composite aHandlerDetailComposite, FormToolkit aToolkit)
    {
        Composite composite = aToolkit.createComposite(aHandlerDetailComposite);
        composite.setLayout(new GridLayout(1, true));

        Label label = aToolkit.createLabel(composite, "Select a handler from the list of handlers on the left.");
        label.setLayoutData(new GridData());
        return composite;
    }

    /**
     * Creates the UI components shown when a script handler is selected.
     *
     * @param aHandlerDetailComposite
     * @param aToolkit
     */
    protected Composite createScriptHandlerUI(Composite aHandlerDetailComposite, FormToolkit aToolkit)
    {
        Composite composite = aToolkit.createComposite(aHandlerDetailComposite);
        composite.setLayout(new GridLayout(2, false));

        Label label = aToolkit.createLabel(composite, "Script:");
        label.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_END | GridData.VERTICAL_ALIGN_CENTER));
        label.setForeground(ZGaeColorUtil.editorLabelForeground(aToolkit));

        mScriptText = aToolkit.createText(composite, "");
        GridData gridData = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING | GridData.VERTICAL_ALIGN_CENTER);
        gridData.widthHint = 300;
        mScriptText.setLayoutData(gridData);

        label = aToolkit.createLabel(composite, "Login:");
        label.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_END | GridData.VERTICAL_ALIGN_CENTER));
        label.setForeground(ZGaeColorUtil.editorLabelForeground(aToolkit));

        mScriptLoginCombo = new Combo(composite, SWT.DROP_DOWN | SWT.READ_ONLY);
        mScriptLoginCombo.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING | GridData.VERTICAL_ALIGN_CENTER));
        mScriptLoginCombo.setItems(new String[] { "None", "Required", "Admin" });
        mScriptLoginCombo.select(0);

        return composite;
    }

    /**
     * Creates the UI components shown when a static directory handler is selected.
     *
     * @param aHandlerDetailComposite
     * @param aToolkit
     */
    protected Composite createStaticDirectoryHandlerUI(Composite aHandlerDetailComposite, FormToolkit aToolkit)
    {
        Composite composite = aToolkit.createComposite(aHandlerDetailComposite);
        composite.setLayout(new GridLayout(2, false));

        Label label = aToolkit.createLabel(composite, "Directory:");
        label.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_END | GridData.VERTICAL_ALIGN_CENTER));
        label.setForeground(ZGaeColorUtil.editorLabelForeground(aToolkit));

        mStaticDirDirectoryText = aToolkit.createText(composite, "");
        GridData gridData = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING | GridData.VERTICAL_ALIGN_CENTER);
        gridData.widthHint = 300;
        mStaticDirDirectoryText.setLayoutData(gridData);

        label = aToolkit.createLabel(composite, "Login:");
        label.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_END | GridData.VERTICAL_ALIGN_CENTER));
        label.setForeground(ZGaeColorUtil.editorLabelForeground(aToolkit));

        mStaticDirLoginCombo = new Combo(composite, SWT.DROP_DOWN | SWT.READ_ONLY);
        mStaticDirLoginCombo.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING | GridData.VERTICAL_ALIGN_CENTER));
        mStaticDirLoginCombo.setItems(new String[] { "None", "Required", "Admin" });
        mStaticDirLoginCombo.select(0);

        label = aToolkit.createLabel(composite, "Mime Type:");
        label.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_END | GridData.VERTICAL_ALIGN_CENTER));
        label.setForeground(ZGaeColorUtil.editorLabelForeground(aToolkit));

        mStaticDirMimeTypeText = aToolkit.createText(composite, "");
        gridData = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING | GridData.VERTICAL_ALIGN_CENTER);
        gridData.widthHint = 200;
        mStaticDirMimeTypeText.setLayoutData(gridData);

        label = aToolkit.createLabel(composite, "Expiration:");
        label.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_END | GridData.VERTICAL_ALIGN_CENTER));
        label.setForeground(ZGaeColorUtil.editorLabelForeground(aToolkit));

        mStaticDirExpirationText = aToolkit.createText(composite, "");
        gridData = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING | GridData.VERTICAL_ALIGN_CENTER);
        gridData.widthHint = 150;
        mStaticDirExpirationText.setLayoutData(gridData);

        return composite;
    }

    /**
     * Creates the UI components shown when a static file pattern handler is selected.
     *
     * @param aHandlerDetailComposite
     * @param aToolkit
     */
    protected Composite createStaticFilePatternHandlerUI(Composite aHandlerDetailComposite, FormToolkit aToolkit)
    {
        Composite composite = aToolkit.createComposite(aHandlerDetailComposite);
        composite.setLayout(new GridLayout(2, false));

        Label label = aToolkit.createLabel(composite, "File Pattern:");
        label.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_END | GridData.VERTICAL_ALIGN_CENTER));
        label.setForeground(ZGaeColorUtil.editorLabelForeground(aToolkit));

        mStaticFilesPatternText = aToolkit.createText(composite, "");
        GridData gridData = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING | GridData.VERTICAL_ALIGN_CENTER);
        gridData.widthHint = 300;
        mStaticFilesPatternText.setLayoutData(gridData);

        label = aToolkit.createLabel(composite, "Login:");
        label.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_END | GridData.VERTICAL_ALIGN_CENTER));
        label.setForeground(ZGaeColorUtil.editorLabelForeground(aToolkit));

        mStaticFilesLoginCombo = new Combo(composite, SWT.DROP_DOWN | SWT.READ_ONLY);
        mStaticFilesLoginCombo.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING | GridData.VERTICAL_ALIGN_CENTER));
        mStaticFilesLoginCombo.setItems(new String[] { "None", "Required", "Admin" });
        mStaticFilesLoginCombo.select(0);

        label = aToolkit.createLabel(composite, "Upload:");
        label.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_END | GridData.VERTICAL_ALIGN_CENTER));
        label.setForeground(ZGaeColorUtil.editorLabelForeground(aToolkit));

        mStaticFilesUploadText = aToolkit.createText(composite, "");
        gridData = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING | GridData.VERTICAL_ALIGN_CENTER);
        gridData.widthHint = 300;
        mStaticFilesUploadText.setLayoutData(gridData);

        label = aToolkit.createLabel(composite, "Mime Type:");
        label.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_END | GridData.VERTICAL_ALIGN_CENTER));
        label.setForeground(ZGaeColorUtil.editorLabelForeground(aToolkit));

        mStaticFilesMimeTypeText = aToolkit.createText(composite, "");
        gridData = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING | GridData.VERTICAL_ALIGN_CENTER);
        gridData.widthHint = 200;
        mStaticFilesMimeTypeText.setLayoutData(gridData);

        label = aToolkit.createLabel(composite, "Expiration:");
        label.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_END | GridData.VERTICAL_ALIGN_CENTER));
        label.setForeground(ZGaeColorUtil.editorLabelForeground(aToolkit));

        mStaticFilesExpirationText = aToolkit.createText(composite, "");
        gridData = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING | GridData.VERTICAL_ALIGN_CENTER);
        gridData.widthHint = 150;
        mStaticFilesExpirationText.setLayoutData(gridData);

        return composite;
    }

    /**
     * Creates the handlers control.
     *
     * @param aHandlersComposite
     */
    protected FilteredTree createHandlersControl(Composite aHandlersComposite)
    {
        int styleBits = SWT.SINGLE | SWT.H_SCROLL | SWT.V_SCROLL | SWT.BORDER | SWT.FULL_SELECTION;
        PatternFilter filter = new ZGaeHandlerPatternFilter();
        FilteredTree handlersTree = new FilteredTree(aHandlersComposite, styleBits, filter);

        TreeViewer treeViewer = handlersTree.getViewer();
        treeViewer.setLabelProvider(new ZGaeHandlerLabelProvider());
        treeViewer.setContentProvider(new ZGaeHandlerContentProvider());
        treeViewer.setComparator(new ZGaeHandlerComparator());
        treeViewer.setInput(getModel());

        handlersTree.getFilterControl().setText("");

        return handlersTree;
    }

    /**
     * Creates the handler buttons (add, remove, up, down).
     *
     * @param aHandlersComposite
     * @param aToolkit
     */
    protected Composite createHandlerButtons(Composite aHandlersComposite, FormToolkit aToolkit)
    {
        Composite handlerButtonsComposite = aToolkit.createComposite(aHandlersComposite);
        GridLayout layout = new GridLayout(1, true);
        handlerButtonsComposite.setLayout(layout);

        mAddButton = aToolkit.createButton(handlerButtonsComposite, "Add...", SWT.NORMAL);
        mAddButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        mRemoveButton = aToolkit.createButton(handlerButtonsComposite, "Remove", SWT.NORMAL);
        mRemoveButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        mMoveUpButton = aToolkit.createButton(handlerButtonsComposite, "Up", SWT.NORMAL);
        mMoveUpButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        mMoveDownButton = aToolkit.createButton(handlerButtonsComposite, "Down", SWT.NORMAL);
        mMoveDownButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

        return handlerButtonsComposite;
    }

    /**
     * Configure the various change listeners on the widgets.
     */
    protected void configureChangeListeners()
    {
        mHandlerList.getFilterControl().addModifyListener(new ModifyListener()
        {
            /**
             * @see org.eclipse.swt.events.ModifyListener#modifyText(org.eclipse.swt.events.ModifyEvent)
             */
            public void modifyText(ModifyEvent aE)
            {
                updateButtonStates();
            }
        });

        // When the user clicks a different handler
        //////////////////////////////////////////////
        final TreeViewer viewer = mHandlerList.getViewer();
        viewer.addSelectionChangedListener(new ISelectionChangedListener()
        {
            /**
             * @see org.eclipse.jface.viewers.ISelectionChangedListener#selectionChanged(org.eclipse.jface.viewers.SelectionChangedEvent)
             */
            public void selectionChanged(SelectionChangedEvent aEvent)
            {
                ITreeSelection selection = (ITreeSelection) aEvent.getSelection();
                updateHandlerUI(selection);
                updateButtonStates();
            }
        });
        mAddButton.addSelectionListener(new SelectionAdapter()
        {
            @Override
            public void widgetSelected(SelectionEvent aE)
            {
                addHandler();
            }
        });
        mRemoveButton.addSelectionListener(new SelectionAdapter()
        {
            @Override
            public void widgetSelected(SelectionEvent aE)
            {
                removeHandler();
            }
        });
        mMoveUpButton.addSelectionListener(new SelectionAdapter()
        {
            @Override
            public void widgetSelected(SelectionEvent aE)
            {
                moveUp();
            }
        });
        mMoveDownButton.addSelectionListener(new SelectionAdapter()
        {
            @Override
            public void widgetSelected(SelectionEvent aE)
            {
                moveDown();
            }
        });

        // The script handler UI control listeners
        /////////////////////////////////////////////
        mScriptText.addFocusListener(new FocusAdapter()
        {
            @Override
            public void focusLost(FocusEvent aEvent)
            {
                doTextControlFocusLost(mScriptHandlerModel, mScriptText, "script", "Edit Script");
            }
        });
        mScriptLoginCombo.addSelectionListener(new SelectionAdapter()
        {
            @Override
            public void widgetSelected(SelectionEvent aEvent)
            {
                doLoginComboControlSelection(mScriptHandlerModel, mScriptLoginCombo, "login", "Edit Login");
            }
        });

        // The static directory handler UI control listeners
        ////////////////////////////////////////////////////////
        mStaticDirDirectoryText.addFocusListener(new FocusAdapter()
        {
            @Override
            public void focusLost(FocusEvent aEvent)
            {
                doTextControlFocusLost(mStaticDirectoryHandlerModel, mStaticDirDirectoryText, "staticDir", "Edit Directory");
            }
        });
        mStaticDirLoginCombo.addSelectionListener(new SelectionAdapter()
        {
            @Override
            public void widgetSelected(SelectionEvent aEvent)
            {
                doLoginComboControlSelection(mStaticDirectoryHandlerModel, mStaticDirLoginCombo, "login", "Edit Login");
            }
        });
        mStaticDirExpirationText.addFocusListener(new FocusAdapter()
        {
            @Override
            public void focusLost(FocusEvent aEvent)
            {
                doTextControlFocusLost(mStaticDirectoryHandlerModel, mStaticDirExpirationText, "expiration", "Edit Expiration");
            }
        });
        mStaticDirMimeTypeText.addFocusListener(new FocusAdapter()
        {
            @Override
            public void focusLost(FocusEvent aEvent)
            {
                doTextControlFocusLost(mStaticDirectoryHandlerModel, mStaticDirMimeTypeText, "mimeType", "Edit Mime Type");
            }
        });

        // The static file pattern handler UI control listeners
        ///////////////////////////////////////////////////////////
        mStaticFilesPatternText.addFocusListener(new FocusAdapter()
        {
            @Override
            public void focusLost(FocusEvent aEvent)
            {
                doTextControlFocusLost(mStaticFilesHandlerModel, mStaticFilesPatternText, "staticFiles", "Edit File Pattern");
            }
        });
        mStaticFilesLoginCombo.addSelectionListener(new SelectionAdapter()
        {
            @Override
            public void widgetSelected(SelectionEvent aEvent)
            {
                doLoginComboControlSelection(mStaticFilesHandlerModel, mStaticFilesLoginCombo, "login", "Edit Login");
            }
        });
        mStaticFilesExpirationText.addFocusListener(new FocusAdapter()
        {
            @Override
            public void focusLost(FocusEvent aEvent)
            {
                doTextControlFocusLost(mStaticFilesHandlerModel, mStaticFilesExpirationText, "expiration", "Edit Expiration");
            }
        });
        mStaticFilesMimeTypeText.addFocusListener(new FocusAdapter()
        {
            @Override
            public void focusLost(FocusEvent aEvent)
            {
                doTextControlFocusLost(mStaticFilesHandlerModel, mStaticFilesMimeTypeText, "mimeType", "Edit Mime Type");
            }
        });
        mStaticFilesUploadText.addFocusListener(new FocusAdapter()
        {
            @Override
            public void focusLost(FocusEvent aEvent)
            {
                doTextControlFocusLost(mStaticFilesHandlerModel, mStaticFilesUploadText, "upload", "Edit Upload Pattern");
            }
        });
    }

    /**
     * Called when the user clicks the Add button to add a new handler.
     */
    protected void addHandler()
    {
        ZGaeAddHandlerDialog dialog = new ZGaeAddHandlerDialog(getEditorSite().getShell());
        if (dialog.open() == Dialog.OK)
        {
            ZGaeAddHandlerInfo handlerInfo = dialog.getAddHandlerInfo();
            doAddHandler(handlerInfo);
        }
    }

    /**
     * Do the work of adding the handler.  This is done within an Operation
     * so that it can be undone/redone.
     *
     * @param aHandlerInfo
     */
    private void doAddHandler(ZGaeAddHandlerInfo aHandlerInfo)
    {
        IZGaeHandlerModel handlerModel = createHandlerModel(aHandlerInfo);
        IZGaeUndoableOperation operation = new ZGaeAddHandlerOperation("Add Handler", getModel(),
                handlerModel, new IZGaeUIUpdater()
        {
            public void updateUIForExecute(Object aValue)
            {
                updateUIAfterHandlerAddOrRemove((Integer) aValue);
            }

            public void updateUIForRedo(Object aValue)
            {
                updateUIForExecute(aValue);
            }

            public void updateUIForUndo(Object aValue)
            {
                updateUIAfterHandlerAddOrRemove(-1);
            }
        });
        executeOperation(operation);
    }

    /**
     * Creates the proper handler model from the handler info gotten from
     * the user.
     *
     * @param aHandlerInfo
     */
    private IZGaeHandlerModel createHandlerModel(ZGaeAddHandlerInfo aHandlerInfo)
    {
        ZGaeAbstractHandlerModel handlerModel = null;
        if (aHandlerInfo.getHandlerType() == TZGaeHandlerType.Script)
        {
            handlerModel = new ZGaeScriptHandlerModel();
            ((ZGaeScriptHandlerModel) handlerModel).setScript("path/to/script.py");
        }
        else if (aHandlerInfo.getHandlerType() == TZGaeHandlerType.StaticDirectory)
        {
            handlerModel = new ZGaeStaticDirectoryHandlerModel();
            ((ZGaeStaticDirectoryHandlerModel) handlerModel).setStaticDir("path/to/directory");
        }
        else
        {
            handlerModel = new ZGaeStaticFilePatternHandlerModel();
            ((ZGaeStaticFilePatternHandlerModel) handlerModel).setStaticFiles("(file)?(pattern)*");
        }

        handlerModel.setUrl(aHandlerInfo.getUrl());
        return handlerModel;
    }

    /**
     * Called when the user clicks the Remove button to remove the selected
     * handler.
     */
    protected void removeHandler()
    {
        TreeSelection selection = (TreeSelection) mHandlerList.getViewer().getSelection();
        IZGaeHandlerModel handlerModel = (IZGaeHandlerModel) selection.getFirstElement();
        IZGaeUndoableOperation operation = new ZGaeRemoveHandlerOperation("Remove Handler", getModel(),
                handlerModel, new IZGaeUIUpdater()
        {
            public void updateUIForExecute(Object aValue)
            {
                updateUIAfterHandlerAddOrRemove(-1);
            }

            public void updateUIForRedo(Object aValue)
            {
                updateUIForExecute(aValue);
            }

            public void updateUIForUndo(Object aValue)
            {
                updateUIAfterHandlerAddOrRemove((Integer) aValue);
            }
        });
        executeOperation(operation);
    }

    /**
     * Called when the user wants to move the selected handler up in the list.
     */
    protected void moveUp()
    {
        doMove(-1);
    }

    /**
     * Called when the user wants to move the selected handler down in the list.
     */
    protected void moveDown()
    {
        doMove(1);
    }

    /**
     * Do the move of a handler - either -1 or 1 will be the argument.
     */
    protected void doMove(int aMoveAmount)
    {
        TreeSelection selection = (TreeSelection) mHandlerList.getViewer().getSelection();
        IZGaeHandlerModel handlerModel = (IZGaeHandlerModel) selection.getFirstElement();
        IZGaeUndoableOperation operation = new ZGaeMoveHandlerOperation("Handler Move", getModel(),
            handlerModel, aMoveAmount, new ZGaeUIUpdaterAdapter()
        {
            /**
             * @see org.zoundry.gaedt.ui.editors.operations.ZGaeUIUpdaterAdapter#updateUI(java.lang.Object)
             */
            @Override
            protected void updateUI(Object aValue)
            {
                int newIndex = (Integer) aValue;
                updateUIAfterHanderMove(newIndex);
            }

            /**
             * @see org.zoundry.gaedt.ui.editors.operations.ZGaeUIUpdaterAdapter#updateUIForExecute(java.lang.Object)
             */
            @Override
            public void updateUIForExecute(Object aValue)
            {
                updateUI(aValue);
            }
        });
        executeOperation(operation);
    }

    /**
     * Does the UI stuff needed when a move (up or down) is executed.
     *
     * @param aNewSelectionIndex
     */
    protected void updateUIAfterHanderMove(int aNewSelectionIndex)
    {
        // Refresh the tree UI
        mHandlerList.getViewer().refresh();
        // Reselect the same item in the tree
        TreeItem item = mHandlerList.getViewer().getTree().getItem(aNewSelectionIndex);
        mHandlerList.getViewer().getTree().select(item);

        // Update the handler UI, since we may have just made a selection change
        updateHandlerUI((ITreeSelection) mHandlerList.getViewer().getSelection());

        // Update the button states
        updateButtonStates();
        // Keep the focus on the tree
        mHandlerList.getViewer().getTree().setFocus();
    }

    /**
     * Called both when a handler is removed and restored (via undo).  This
     * method is responsible for updating the UI.
     *
     * @param aRestoredSelectionIndex
     */
    protected void updateUIAfterHandlerAddOrRemove(int aRestoredSelectionIndex)
    {
        // Refresh the tree UI
        mHandlerList.getViewer().refresh();
        // Select the restored item (if any)
        if (aRestoredSelectionIndex != -1)
        {
            TreeItem item = mHandlerList.getViewer().getTree().getItem(aRestoredSelectionIndex);
            mHandlerList.getViewer().getTree().select(item);
        }

        // Update the handler UI, since we have just made a selection change
        updateHandlerUI((ITreeSelection) mHandlerList.getViewer().getSelection());

        // Update the button states
        updateButtonStates();
        // Keep the focus on the tree
        mHandlerList.getViewer().getTree().setFocus();
    }

    /**
     * Creates and executes an operation to update a model from a change in a
     * {@link Text} control.
     *
     * @param aHandlerModel
     * @param aTextControl
     * @param aBeanPropertyName
     * @param aUndoLabel
     */
    protected void doTextControlFocusLost(final IZGaeHandlerModel aHandlerModel, final Text aTextControl,
            String aBeanPropertyName, String aUndoLabel)
    {
        IZGaeUndoableOperation operation = new ZGaeSetBeanPropertyOperation(
                aHandlerModel, aBeanPropertyName, aTextControl.getText(),
                aUndoLabel, new ZGaeUIUpdaterAdapter()
        {
            @Override
            protected void updateUI(Object aValue)
            {
                aTextControl.setText(String.valueOf(aValue));
            }
        });
        executeOperation(operation);
    }

    /**
     * Creates and executes an operation to update a model from a change
     * in a {@link Combo} control.
     *
     * @param aHandlerModel
     * @param aComboControl
     * @param aBeanPropertyName
     * @param aUndoLabel
     */
    protected void doLoginComboControlSelection(IZGaeHandlerModel aHandlerModel, final Combo aComboControl,
            String aBeanPropertyName, String aUndoLabel)
    {
        int selectionIdx = aComboControl.getSelectionIndex();
        TZGaeHandlerLoginType newLoginType = sLoginMapInv.get(selectionIdx);
        IZGaeUndoableOperation operation = new ZGaeSetBeanPropertyOperation(
                aHandlerModel, aBeanPropertyName, newLoginType,
                aUndoLabel, new ZGaeUIUpdaterAdapter()
        {
            @Override
            protected void updateUI(Object aValue)
            {
                aComboControl.select(sLoginMap.get(aValue));
            }
        });
        executeOperation(operation);
    }

    /**
     * Populates the script handler UI controls with data from the given
     * script handler model.  This method is called when a script handler
     * is selected from the list (in the UI).
     *
     * @param aHandlerModel
     */
    protected void populateScriptHandlerUI(ZGaeScriptHandlerModel aHandlerModel)
    {
        mScriptText.setText(aHandlerModel.getScript());
        int selectionIdx = sLoginMap.get(aHandlerModel.getLogin());
        mScriptLoginCombo.select(selectionIdx);
    }

    /**
     * Populates the static directory handler UI controls with data from the
     * given handler model. This method is called when a static directory
     * handler is selected from the list (in the UI).
     *
     * @param aHandlerModel
     */
    protected void populateStaticDirectoryHandlerUI(ZGaeStaticDirectoryHandlerModel aHandlerModel)
    {
        mStaticDirDirectoryText.setText(aHandlerModel.getStaticDir());
        int selectionIdx = sLoginMap.get(aHandlerModel.getLogin());
        mStaticDirLoginCombo.select(selectionIdx);
        mStaticDirMimeTypeText.setText(ZGaeUtil.getSafeString(aHandlerModel.getMimeType()));
        mStaticDirExpirationText.setText(ZGaeUtil.getSafeString(aHandlerModel.getExpiration()));
    }

    /**
     * Populates the static file pattern handler UI controls with data from the
     * given handler model. This method is called when a static file pattern
     * handler is selected from the list (in the UI).
     *
     * @param aHandlerModel
     */
    protected void populateStaticFilePatternHandlerUI(ZGaeStaticFilePatternHandlerModel aHandlerModel)
    {
        mStaticFilesPatternText.setText(aHandlerModel.getStaticFiles());
        int selectionIdx = sLoginMap.get(aHandlerModel.getLogin());
        mStaticFilesLoginCombo.select(selectionIdx);
        mStaticFilesUploadText.setText(ZGaeUtil.getSafeString(aHandlerModel.getUpload()));
        mStaticFilesMimeTypeText.setText(ZGaeUtil.getSafeString(aHandlerModel.getMimeType()));
        mStaticFilesExpirationText.setText(ZGaeUtil.getSafeString(aHandlerModel.getExpiration()));
    }
}
